/*
 *  Class name: EntityCriteriaQueryBuilder.java
 *  Version: 1.0
 *  Date: 18.2.2014
 */
package cz.muni.fi.pa165.soccergamesarchive.persistencelayer.querybuilders;

import cz.muni.fi.pa165.soccergamesarchive.structures.FilterValue;
import cz.muni.fi.pa165.soccergamesarchive.structures.RestrictionOperator;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.TemporalType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Root;

import static java.util.Collections.emptyList;
import static java.util.Locale.getDefault;

/**
 *
 * @author Michal Ďuriš
 * @param <T> entity type
 */
public abstract class EntityCriteriaQueryBuilder<T> {

    protected final Class<T> entityClass;

    public EntityCriteriaQueryBuilder(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

    protected abstract Expression<Boolean> getFieldFilterExpression(
            String propertyName, CriteriaBuilder criteriaBuilder,
            Root<T> entityRoot);

    protected abstract Expression<?> getFieldOrderExpression(String propertyName,
                                                             Root<T> entityRoot);

    public TypedQuery<T> createEntitySelectCriteriaQuery(
            EntityManager entityManager,
            Map<String, Object> filterMap,
            Map<String, Boolean> orderMap) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

        CriteriaQuery<T> entityQuery = criteriaBuilder.createQuery(
                entityClass);

        Root<T> entityRoot = entityQuery.from(entityClass);

        Expression<Boolean> whereExpresion = processFilterMap(filterMap,
                criteriaBuilder, entityRoot);

        if (whereExpresion != null) {
            entityQuery.where(whereExpresion);
        }

        List<Order> orders = processOrderMap(orderMap, criteriaBuilder,
                entityRoot);

        if (orders != null && !orders.isEmpty()) {
            entityQuery.orderBy(orders);
        }

        return setQueryParametres(filterMap, entityManager.createQuery(
                entityQuery));
    }

    public TypedQuery<Long> createEntityCountCriteriaQuery(
            EntityManager entityManager, Map<String, Object> filterMap) {

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

        CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);

        Root<T> entityRoot = countQuery.from(entityClass);

        Expression<Boolean> whereExpresion = processFilterMap(filterMap,
                criteriaBuilder, entityRoot);

        if (whereExpresion != null) {
            countQuery.where(whereExpresion);
        }

        return setQueryParametres(filterMap, entityManager.createQuery(
                countQuery.select(criteriaBuilder.count(entityRoot))));
    }

    protected Expression<Boolean> processFilterMap(Map<String, Object> filterMap,
                                                   CriteriaBuilder criteriaBuilder,
                                                   Root<T> entityRoot) {

        if (filterMap != null && !filterMap.isEmpty()) {

            return getFilterExpression(filterMap, criteriaBuilder, entityRoot);
        }

        return null;
    }

    private Expression<Boolean> getFilterExpression(
            Map<String, Object> filterMap, CriteriaBuilder criteriaBuilder,
            Root<T> entityRoot) {

        Expression<Boolean> andFilterExpression = null;
        Expression<Boolean> orFilterExpression = null;

        for (Map.Entry<String, Object> filterEntry : filterMap.entrySet()) {

            if ((filterEntry.getKey() == null || filterEntry.getKey().isEmpty())
                || !isFilterValueValid(filterEntry.getValue())) {
                continue;
            }

            Expression<Boolean> fieldFilterExpression = getFieldFilterExpression(
                    filterEntry.getKey(), criteriaBuilder,
                    entityRoot);

            if (fieldFilterExpression != null) {

                RestrictionOperator filterFieldOperator = ((FilterValue) filterEntry
                                                           .getValue())
                        .getRestrictionOperator();

                if (filterFieldOperator == RestrictionOperator.AND) {

                    andFilterExpression = processFilterFieldExpression(
                            andFilterExpression, fieldFilterExpression,
                            filterFieldOperator, criteriaBuilder);

                } else if (filterFieldOperator == RestrictionOperator.OR) {

                    orFilterExpression = processFilterFieldExpression(
                            orFilterExpression, fieldFilterExpression,
                            filterFieldOperator, criteriaBuilder);
                }
            }
        }

        return generateFilterExpression(andFilterExpression, orFilterExpression,
                criteriaBuilder);
    }

    private static boolean isFilterValueValid(Object value) {

        if (value != null) {

            if (value instanceof FilterValue) {

                FilterValue filterValue = (FilterValue) value;

                if (filterValue.getValue() != null) {

                    if (filterValue.getValue() instanceof String
                        && ((String) filterValue.getValue()).isEmpty()) {

                        return false;
                    }

                    return filterValue.getRestrictionOperator() != null;
                }

            } else {
                //TODO: LOGGER
                System.err.println(EntityCriteriaQueryBuilder.class.getName()
                                   + ": Unsupported filter value type -> " + value
                        .getClass()
                                   + ". Value will be not processed. ");
            }
        }

        return false;
    }

    private Expression<Boolean> processFilterFieldExpression(
            Expression<Boolean> filterExpression,
            Expression<Boolean> filterFieldExpression,
            RestrictionOperator operator, CriteriaBuilder criteriaBuilder) {

        if (filterExpression == null) {
            filterExpression = filterFieldExpression.as(Boolean.class);
        } else {
            switch (operator) {
                case OR:
                    filterExpression = criteriaBuilder.or(filterExpression,
                            filterFieldExpression.as(Boolean.class));
                    break;
                case AND:
                    filterExpression = criteriaBuilder.and(filterExpression,
                            filterFieldExpression.as(Boolean.class));
                    break;
                default:
                    throw new IllegalArgumentException(getClass().getName()
                                                       + ": Unknown restriction operator !");
            }
        }

        return filterExpression;
    }

    private Expression<Boolean> generateFilterExpression(
            Expression<Boolean> andFilterExpression,
            Expression<Boolean> orFilterExpression,
            CriteriaBuilder criteriaBuilder) {

        if (andFilterExpression != null && orFilterExpression != null) {
            return criteriaBuilder.and(andFilterExpression, orFilterExpression);
        }

        return orFilterExpression != null ? orFilterExpression
               : andFilterExpression;
    }

    protected List<Order> processOrderMap(Map<String, Boolean> orderMap,
                                          CriteriaBuilder criteriaBuilder,
                                          Root<T> entityRoot) {

        if (orderMap != null && !orderMap.isEmpty()) {

            return getOrders(orderMap, criteriaBuilder, entityRoot);
        }

        return emptyList();
    }

    private List<Order> getOrders(Map<String, Boolean> orderMap,
                                  CriteriaBuilder criteriaBuilder,
                                  Root<T> entityRoot) {

        List<Order> orders = new ArrayList<>();
        Order fieldOrder;

        for (Map.Entry<String, Boolean> entry : orderMap.entrySet()) {

            if (entry.getValue()) {
                fieldOrder = criteriaBuilder.asc(getFieldOrderExpression(entry
                        .getKey(),
                        entityRoot));
            } else {
                fieldOrder = criteriaBuilder.desc(getFieldOrderExpression(entry
                        .getKey(),
                        entityRoot));
            }

            orders.add(fieldOrder);
        }

        return orders;
    }

    protected static <X> TypedQuery<X> setQueryParametres(
            Map<String, Object> filterMap,
            TypedQuery<X> query) {

        if (filterMap != null && !filterMap.isEmpty()) {

            for (Map.Entry<String, Object> entry : filterMap.entrySet()) {

                if (entry.getKey() == null || entry.getKey().isEmpty()) {
                    continue;
                }

                if (isFilterValueValid(entry.getValue())) {

                    FilterValue filterValue = (FilterValue) entry.getValue();

                    if (filterValue.getValue() instanceof String) {

                        String stringValueParameter = (String) filterValue
                                .getValue();

                        query.setParameter(entry.getKey(),
                                stringValueParameter.trim().toLowerCase(
                                        getDefault()) + "%");
                    }

                    if (filterValue.getValue() instanceof Long) {

                        query.setParameter(entry.getKey(), (Long) filterValue
                                .getValue());
                    }

                    if (filterValue.getValue() instanceof Date) {

                        query.setParameter(entry.getKey(), (Date) filterValue
                                .getValue(), TemporalType.DATE);
                    }
                }
            }
        }

        return query;
    }

}
